#!/usr/bin/env bash
# Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.

# Script to compile a benchmark using dart2wasm. Assumes the Dart repo's
# directory structure.

set -e

function follow_links() {
  file="$1"
  while [ -h "$file" ]; do
    # On Mac OS, readlink -f doesn't work.
    file="$(readlink "$file")"
  done
  echo "$file"
}

function host_arch() {
  # Use uname to determine the host architecture.
  case `uname -m` in
    x86_64)
    echo "X64"
    ;;
    aarch64 | arm64 | armv8*)
    echo "ARM64"
    ;;
    *)
    echo "Unknown host architecture" `uname -m` >&2
    exit 1
    ;;
  esac
}

# Unlike $0, $BASH_SOURCE points to the absolute path of this file.
PROG_NAME="$(follow_links "$BASH_SOURCE")"

# Handle the case where dart-sdk/bin has been symlinked to.
PROG_DIR="$(cd "${PROG_NAME%/*}" ; pwd -P)"
SDK_DIR="$(cd "${PROG_DIR}/../../.." ; pwd -P)"

# Locate build directory, containing executables, snapshots and platform dill.
if [[ `uname` == 'Darwin' ]]; then
  OUT_DIR="$SDK_DIR/xcodebuild"
else
  OUT_DIR="$SDK_DIR/out"
fi
HOST_ARCH="$(host_arch)"
DART_CONFIGURATION=${DART_CONFIGURATION:-Release$HOST_ARCH}
BIN_DIR="$OUT_DIR/$DART_CONFIGURATION"

BINARYEN="$BIN_DIR/wasm-opt"
DART="$BIN_DIR/dart"
DART_AOT_RUNTIME="$BIN_DIR/dartaotruntime"
LIBRARIES_JSON_ARG="--libraries-spec=$SDK_DIR/sdk/lib/libraries.json"

function find_flags {
  echo -en "$(sed -n "/$1 =/,/end of $1/ p" $SDK_DIR/pkg/dartdev/lib/src/commands/compile.dart  | sed  '1d' | sed '$d' | tr '\n' ' ' | sed 's#\s\+# #g' | sed 's#^\s\+##' | sed 's#\s\+$##')"
}

RUN_BINARYEN=1
RUN_SRC=0
GENERATE_SOURCE_MAP=1
COMPILE_BENCHMARK_BASE_NAME=""
PLATFORM_FILENAME="$BIN_DIR/dart2wasm_platform.dill"
SNAPSHOT_NAME="dart2wasm"

# All arguments will be passed along to dart2wasm except specially recognized
# flags.
VM_ARGS=()
DART2WASM_ARGS=("--require-js-string-builtin")
DART_FILE=""
OUTPUT_FILE=""
while [ $# -gt 0 ]; do
    case "$1" in
      --compile-benchmark=*)
      COMPILE_BENCHMARK_BASE_NAME=$(echo $1 | sed 's/--compile-benchmark=//g')
      shift
      ;;

    --src)
      RUN_SRC=1
      shift
      ;;

    --js-compatibility)
      PLATFORM_FILENAME="$BIN_DIR/dart2wasm_js_compatibility_platform.dill"
      DART2WASM_ARGS+=("--js-compatibility")
      shift
      ;;

    --compiler-asserts)
      SNAPSHOT_NAME="dart2wasm_asserts"
      VM_ARGS+=("--enable-asserts")
      shift
      ;;

    -O0 | --optimization-level=0)
      DART2WASM_ARGS+=("-O0")
      RUN_BINARYEN=0
      shift
      ;;

    --extra-compiler-option=--platform=*)
      PLATFORM_FILENAME="${1#--extra-compiler-option=--platform=}"
      shift
      ;;

    --extra-compiler-option=--no-js-compatibility)
      PLATFORM_FILENAME="$BIN_DIR/dart2wasm_platform.dill"
      DART2WASM_ARGS+=("--no-js-compatibility")
      shift
      ;;

    --extra-compiler-option=*)
      DART2WASM_ARGS+=("${1#--extra-compiler-option=}")
      shift
      ;;

    --no-source-maps)
      GENERATE_SOURCE_MAP=0
      shift
      ;;

    -o)
      shift
      OUTPUT_FILE="$1"
      shift
      ;;

    --* | -*)
      DART2WASM_ARGS+=("$1")
      shift
      ;;

    *)
      if [ -z "$DART_FILE" ]; then
        DART_FILE="$1"
        shift
      else
        if [ -z "$OUTPUT_FILE" ]; then
          OUTPUT_FILE="$1"
          shift
        else
          echo "Unexpected argument $1"
          exit 1
        fi
      fi
      ;;
  esac
done

if [ -z "$DART_FILE" -o -z "$OUTPUT_FILE" ]; then
  echo "Expected <file.dart> <file.wasm>"
  exit 1
fi


PLATFORM_ARG="--platform=$PLATFORM_FILENAME"
DART2WASM_AOT_SNAPSHOT="$BIN_DIR/$SNAPSHOT_NAME.snapshot"
DART2WASM_SRC="$SDK_DIR/pkg/dart2wasm/bin/dart2wasm.dart"

function measure() {
  set +e
  RESULT=$( { /usr/bin/time --format="\nMemory: %M KB, Time: %e s" $@; } 2>&1 )
  EXIT=$?
  if [ $EXIT -ne 0 ]; then
    echo "Running $@ resulted in exit code $EXIT"
    echo "$RESULT"
    exit 1
  fi
  set -e
  MEMORY=$(echo "$RESULT" | tail -n1 | sed 's/Memory: \([0-9.]\+\) KB, Time: \([0-9.]\+\) s.*/\1/g')
  MEMORY=$(($MEMORY * 1024))
  TIME=$(echo "$RESULT" | tail -n1 | sed 's/Memory: \([0-9.]\+\) KB, Time: \([0-9.]\+\) s.*/\2/g')
}

function measure_size() {
  SIZE=$(cat $1 | wc -c)
  GZIP_SIZE=$(cat $1 | gzip -c6 | wc -c)
}

function run_if_binaryen_enabled() {
  if [ $RUN_BINARYEN -eq 1 ] && [[ $OUTPUT_FILE == *.wasm ]]; then
    $@
  fi
}

COMPILER_SIZE=0
COMPILER_GZIP_SIZE=0

function run_compiler() {
  if [ $RUN_SRC -eq 1 ]; then
    dart2wasm_command=("$DART" "${VM_ARGS[@]}" "$DART2WASM_SRC" "$LIBRARIES_JSON_ARG" "${DART2WASM_ARGS[@]}" "--phases=cfe,tfa,codegen" "$DART_FILE" "$OUTPUT_FILE")
  else
    dart2wasm_command=("$DART_AOT_RUNTIME" "${VM_ARGS[@]}" "$DART2WASM_AOT_SNAPSHOT" "$PLATFORM_ARG" "${DART2WASM_ARGS[@]}" "--phases=cfe,tfa,codegen" "$DART_FILE" "$OUTPUT_FILE")
  fi

  if [ -n "$COMPILE_BENCHMARK_BASE_NAME" ]; then
    measure "${dart2wasm_command[@]}"
    COMPILER_TIME=$TIME
    COMPILER_MEMORY=$MEMORY

    measure_size ${OUTPUT_FILE%.wasm}.mjs
    MJS_SIZE=$SIZE
    MJS_GZIP_SIZE=$GZIP_SIZE

    for OUTPUT_FILE in "${OUTPUT_FILE%.wasm}"*.wasm; do
      measure_size $OUTPUT_FILE
      (( COMPILER_SIZE+=$SIZE ))
      (( COMPILER_GZIP_SIZE+=$GZIP_SIZE ))
    done
  else
    "${dart2wasm_command[@]}"
  fi
}

BINARYEN_TIME=0
BINARYEN_MEMORY=0
BINARYEN_SIZE=0
BINARYEN_GZIP_SIZE=0

function run_binaryen() {
  if [ $RUN_SRC -eq 1 ]; then
    opt_command=("$DART" "${VM_ARGS[@]}" "$DART2WASM_SRC" "$LIBRARIES_JSON_ARG" "${DART2WASM_ARGS[@]}" "--phases=opt" "--wasm-opt=$BINARYEN" "$OUTPUT_FILE" "$OUTPUT_FILE")
  else
    opt_command=("$DART_AOT_RUNTIME" "${VM_ARGS[@]}" "$DART2WASM_AOT_SNAPSHOT" "$PLATFORM_ARG" "${DART2WASM_ARGS[@]}" "--phases=opt" "--wasm-opt=$BINARYEN" "$OUTPUT_FILE" "$OUTPUT_FILE")
  fi
  if [ -n "$COMPILE_BENCHMARK_BASE_NAME" ]; then
    # If we're measuring run each binaryen command sequentially.
    measure "${opt_command[@]}"
    BINARYEN_TIME=$(echo "$BINARYEN_TIME + $TIME" | bc)
    BINARYEN_MEMORY=$(($BINARYEN_MEMORY > $MEMORY ? $BINARYEN_MEMORY : $MEMORY ))
    measure_size $OUTPUT_FILE
    BINARYEN_SIZE=$(echo "$BINARYEN_SIZE + $SIZE" | bc)
    BINARYEN_GZIP_SIZE=$(echo "$BINARYEN_GZIP_SIZE + $GZIP_SIZE" | bc)
    MAX_MEMORY=$(($COMPILER_MEMORY > $BINARYEN_MEMORY ? $COMPILER_MEMORY : $BINARYEN_MEMORY ))
    TOTAL_TIME=$(echo "$COMPILER_TIME + $BINARYEN_TIME" | bc)
  else
    "${opt_command[@]}"
  fi
}

run_compiler
run_if_binaryen_enabled run_binaryen

if [ -n "$COMPILE_BENCHMARK_BASE_NAME" ]; then
  # CompileTime
  run_if_binaryen_enabled echo "$COMPILE_BENCHMARK_BASE_NAME.Time.Total(CompileTime): $TOTAL_TIME s"
  echo "$COMPILE_BENCHMARK_BASE_NAME.Time.Dart2Wasm(CompileTime): $COMPILER_TIME s"
  run_if_binaryen_enabled echo "$COMPILE_BENCHMARK_BASE_NAME.Time.Wasm2WasmOpt(CompileTime): $BINARYEN_TIME s"

  # CodeSize
  echo "$COMPILE_BENCHMARK_BASE_NAME.Size.mjs(CodeSize): $MJS_SIZE bytes"
  echo "$COMPILE_BENCHMARK_BASE_NAME.Size.mjs.gz(CodeSize): $MJS_GZIP_SIZE bytes"
  echo "$COMPILE_BENCHMARK_BASE_NAME.Size.wasm(CodeSize): $COMPILER_SIZE bytes"
  echo "$COMPILE_BENCHMARK_BASE_NAME.Size.wasm.gz(CodeSize): $COMPILER_GZIP_SIZE bytes"
  run_if_binaryen_enabled echo "$COMPILE_BENCHMARK_BASE_NAME.Size.wasm.opt(CodeSize): $BINARYEN_SIZE bytes"
  run_if_binaryen_enabled echo "$COMPILE_BENCHMARK_BASE_NAME.Size.wasm.opt.gz(CodeSize): $BINARYEN_GZIP_SIZE bytes"

  # MemoryUse
  run_if_binaryen_enabled echo "$COMPILE_BENCHMARK_BASE_NAME.MemoryUse.Max(MemoryUse): $MAX_MEMORY bytes"
  echo "$COMPILE_BENCHMARK_BASE_NAME.MemoryUse.Dart2Wasm(MemoryUse): $COMPILER_MEMORY bytes"
  run_if_binaryen_enabled echo "$COMPILE_BENCHMARK_BASE_NAME.MemoryUse.Wasm2WasmOpt(MemoryUse): $BINARYEN_MEMORY bytes"
fi
